# -*- coding: utf-8 -*-
#================================================================================================
from django.conf import settings
import passwords, logging, time
from waveapi import robot as robot_api
from waveapi import appengine_robot_runner
from waveapi import element
from google.appengine.api import memcache
from djapp.models import *
from djapp.wave import *
from djapp.annotation import *


class View:
    WAVE_LINK_KEY = "link/wave"
    LEVEL_KEY = 'level'    
    SERVER_RPC_BASE = 'http://www-opensocial.googleusercontent.com/api/rpc'
    OTHER_TAG_NAME = "other"
    BOLD_TEXT = ("style/fontWeight", "bold")
    OTHER_TAG = ('level', '100.other')
    TRIES = 5
    
    def __init__(self):
        self.__robot = None        
        
    def _create_robot(self):
        self.__robot = robot_api.Robot(passwords.ROBOT_NAME,
                              image_url = passwords.IMAGE_URL,
                              profile_url = passwords.PROFILE_URL)
        self.__robot.setup_oauth(passwords.CONSUMER_KEY, passwords.CONSUMER_SECRET, View.SERVER_RPC_BASE)
        appengine_robot_runner.run(self.__robot)
            
    def _get_offset(self, path):
        end1 = self._path_end(path)
        path_copy = path[:]
        path_copy[end1[0]] = None #end1[0] is the last unNone item
        end2 = self._path_end(path_copy)
        return end1[0] - end2[0] - 1
            
    def _path_end(self, path):
        p = len(path) - 1
        while p >= 0 and path[p] is None:
            p -= 1
        return (p, path[p])    
    
    def _get_inline_blip_id(self, json_reply):
        blip_id = None
        for data in json_reply:
            try:
                blip_id = data['data']['newBlipId']
                break
            except (KeyError, ValueError):
                continue
        return blip_id
    
    def _check_catalog_equality(self, query_set, list2):
        """
            Arguments: 
                query_set: QuerySet of Model-object with field 'path' (ListField type). 
            Returns:
                first model-object where 'path' = list2.
        """
        for cat in query_set:
            try:
                if cat.path == list2:
                    return cat
            except:
                return None
        return None
    
    def _try_again(self, tries, func, *args, **kwargs):
        if tries >= 0:
            try:
                res = func(*args, **kwargs)
                if res is None:
                    raise ValueError
                return res
            except ValueError:
                logging.error("Raised valued error (none returned).")
                self._try_again(tries - 1, func, *args, **kwargs)
            except:
                logging.error('Error caught!', exc_info=True)
                self._try_again(tries - 1, func, *args, **kwargs)                
        else:
            logging.error("Error continues, return NONE. Args: %s, kwargs: %s" % (args, kwargs))
            raise ValueError

    
    def _get_path_list(self, path, wave_id):
        """
            Arguments:
                path: path to the blip (i.e. [1,2,None]) which does not exist in database
            Returns:
                tuple of:
                    path list for this path (from the 1st non-existent path next to root to the last)
                    parent: root Category-object.
        """
        logging.info("PATH: %s" % path)
        path_tmp = path[:]; parent = None
        path_list = []
        i = -1 
        while not parent and i*-1 <= len(path_tmp):
            while i*-1 <= len(path_tmp) and path_tmp[i] is None: #skip middle's Nones
                 i -= 1
            if i*-1 <= len(path_tmp):    
                path_list.insert(0, path_tmp[:])
            if i*-1 < len(path_tmp):
                path_tmp[i] = None
                for p in path_tmp:
                    if p or p == 0:
                        parent = Catalog.objects.filter(path = path_tmp, wave_id = wave_id)
                        parent = self._check_catalog_equality(parent, path_tmp)
                        if parent:
                            break
            i -= 1
        return (path_list, parent)   
        
    
    def _create_blip(self, path, wave_id, level): # you fucking disappoint me
        """
        Creates all necessary blips in right position with right names.
        Arguments:
            path to blip (i.e. [1,2,None]) which doesn't exist in datastore
            wave_id of Catalog wave
        Returns:
             last created Catalog-object (leaf)
        """
        #-------------find parent blip-----------------------------------------
        logging.info("Create blip: path: %s, wave_id: %s" % (path, wave_id))
        edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, wave_id = wave_id)
        path_list, parent = self._get_path_list(path, wave_id)
        if parent:
            blip = edited_wave.blips.get(parent.blip_id)
        else:
            blip = edited_wave.root_blip          
        logging.info("Path_list: %s, parent: %s, blipId: %s" %(path_list, parent, blip.blip_id))
        text = u''; annotation = []; line = {}
        #------------find position inside this blip----------------------------
        old_annotation = None
        lvl_index, last_tag_index = self._path_end(path_list[0])
        if View.LEVEL_KEY in blip.annotations.names():
            lvl_in_wave = {}
            for ann in blip.annotations:
                if ann.name == View.LEVEL_KEY:                    
                    value = ann.value.split('.')
                    if int(value[0]) == level[lvl_index]['level']:
                        old_annotation = ann
                        position = ann.end
                        logging.info("Level exists in parent blip, appending tag.")
                        break
                    else:
                        lvl_in_wave[int(value[0])] = (ann.start, ann.end)
            if not old_annotation:
                #find position where to put LVL into this blip:
                logging.info("Level does not exist. Inserting among others.")
                keys = lvl_in_wave.keys(); keys.sort()
                text += '\n'
                j = 0
                while j < len(keys) and level[lvl_index]['level'] > keys[j]:
                    j += 1
                if j > 0:
                    position = lvl_in_wave[keys[j-1]][1] #adding to the bottom of j-1 lvl
                else:
                    position = lvl_in_wave[keys[j]][0]
        else:
            text += '\n'
            if len(blip.text) < 3:
                logging.info("EMPTY BLIP!!! call create_inline_blip")
                blip.all().delete()
                self.__robot.submit(edited_wave)
                cat = self._create_inline_blips(path_list, wave_id, blip.blip_id, level)
                self._fill_blank_blip(cat.blip_id, wave_id)
                return cat
            logging.info("No lvls at all. Append to the end of blip.")
            position = len(blip.text) - 1
        #-------------forming text and annotation--------------------------
        """add annotation 
            name = View.LEVEL_KEY, range = ...,
            value = "%d.%s" % (level[lvl_index]['level'], level[lvl_index]['name'])
        """
        offset = self._get_offset(path_list[0])  
        begin_position = position
        if not old_annotation:
            line[position + len(text)] = offset
            text += '\n' + level[lvl_index]['name'] # View.TAB * offset  
            annotation.append(Annotation(View.BOLD_TEXT, position + 1, position+len(text)))
        line[position + len(text)] = offset + 1   
        text += '\n' + level[lvl_index]['tags'][last_tag_index] # View.TAB * (offset + 1)
        end_position = position + len(text)

        if not old_annotation:
            ann_value = "%d.%s" % (level[lvl_index]['level'], level[lvl_index]['name'])
            annotation.append(Annotation((View.LEVEL_KEY, ann_value), position, position+len(text) + 1))
        else:
            annotation.append(Annotation((old_annotation.name, old_annotation.value), 
                                         old_annotation.start, end_position + 1))
        blip.at(position).insert(text)
        for l in line.keys():
            logging.info("AT: %d, ident: %d" % (l, line[l]))
            if line[l] > 0:
                blip.at(l).replace(element.Line(indent = line[l]))
        blip.insert_inline_blip(end_position) # - 1) #end_position - 1) #ZAEBAL !
        for ann in annotation:
            blip.range(ann.start, ann.end).annotate(ann.name, ann.value)
        for a in blip.annotations:
            if a.name == View.LEVEL_KEY:
                logging.info("%s" % a.serialize())
        blip.range(ann.start, min(ann.end, len(blip.text))).clear_annotation(View.WAVE_LINK_KEY)    
        logging.info("Parent blip. Position: %d, len(blip.text): %d, len(text): %d, blip inserted at: %d, lvl_range: [%d, %d]" % \
                     (position, len(blip.text), len(text), end_position, annotation[-1].start, annotation[-1].end))
        json = self._try_again(View.TRIES, self.__robot.submit, edited_wave)
        new_blip_id = self._get_inline_blip_id(json)
        cat = Catalog(path = path_list[0], name = level[lvl_index]['tags'][last_tag_index], 
                      wave_id = wave_id, blip_id = new_blip_id)
        cat.save()
        if path_list[1:]:
            cat = self._create_inline_blips(path_list[1:], wave_id, new_blip_id, level)
            new_blip_id = cat.blip_id
        self._fill_blank_blip(new_blip_id, wave_id)
        return cat    
            
    def _fill_blank_blip(self, blip, wave_id = None):
        if wave_id:
            edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, wave_id = wave_id)
            blip = edited_wave.blips.get(blip)
        blip.all().delete()
        blip.at(0).insert('\n\n')
        if wave_id:
            self._try_again(View.TRIES, self.__robot.submit, edited_wave)        
        
        
    def _create_inline_blips(self, path_list, wave_id, new_blip_id, level):
        """
            Draw inline-blips with level and tag headers from path_list starting from new_blip_id
        """   
        for path in path_list:
            annotation = []; line = {}     
            edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, wave_id = wave_id)
            blip = edited_wave.blips.get(new_blip_id)
            end = self._path_end(path)
            lvl_index = end[0]
            offset = self._get_offset(path)
            line[3] = offset
            text = '\n\n\n\n'  + level[lvl_index]['name'] #  + View.TAB * offset        
            annotation.append(Annotation(View.BOLD_TEXT, 3, len(text)))
            line[len(text)] = offset + 1
            text += '\n' + level[lvl_index]['tags'][end[1]] #View.TAB * (offset + 1)
            ann_value = "%d.%s" % (level[lvl_index]['level'], level[lvl_index]['name'])
            annotation.append(Annotation((View.LEVEL_KEY, ann_value), 2, len(text) + 1)) 
            blip.at(0).insert(text)
            for l in line.keys():
                logging.info("AT: %d, ident: %d" % (l, line[l]))
                if line[l] > 0:
                    blip.at(l).replace(element.Line(indent = line[l]))
            blip.insert_inline_blip(len(text))
            for ann in annotation:
                blip.range(ann.start, min(len(blip.text), ann.end)).annotate(ann.name, ann.value)
            json = self._try_again(View.TRIES, self.__robot.submit, edited_wave)
            new_blip_id = self._get_inline_blip_id(json)
            cat = Catalog(path = path, name = level[lvl_index]['tags'][end[1]], 
                          wave_id = wave_id, blip_id = new_blip_id)
            cat.save()
        return cat         
    
    def _create_none_blip(self, path, wave_id):
        logging.info("WAVE_ID: %s, type: %s" % (wave_id, type(wave_id)))
        edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, wave_id = wave_id)  
        blip = edited_wave.root_blip
        text = u'\n\nПрочие\n'
        logging.info("creating 'other' blip")
        blip.append(text, [View.BOLD_TEXT, View.OTHER_TAG])
        logging.info("Insert inline blip at position len(blip.text) - 1: %d" % (len(blip.text) - 1) )
        blip.insert_inline_blip(len(blip.text) - 1)
        json = self.__robot.submit(edited_wave)
        new_blip_id = self._get_inline_blip_id(json)
        cat = Catalog.objects.create(path = path, name = View.OTHER_TAG_NAME, 
                          wave_id = wave_id, blip_id = new_blip_id)
        return cat   
        
    
    def add_wave(self, wave, level, category_list, ext_tag):        
        self._create_robot()
        path_list = wave.get_path(level, ext_tag)    
        wave_categories = wave.get_catalog_list(category_list)
        none_path = [None for i in path_list[0]]
        for cat_index in wave_categories:
            wave_id = category_list[cat_index]['wave_id'].strip()
            logging.info("wave_id: %s" % wave_id)
            for path in path_list: 
                blip_set = Catalog.objects.filter(path = path, wave_id = wave_id)
                db_blip = self._check_catalog_equality(blip_set, path)
                if not db_blip:
                    logging.info("NOT DB_BLIP")
                    if path == none_path:
                        logging.info("CREATE NON BLIP")
                        db_blip = self._create_none_blip(path, wave_id) #IMPLEMENT THIS! automatic initialization or checking and initialization
                    else:
                        logging.info("CREATE COMMON BLIP")
                        db_blip = self._create_blip(path, wave_id, level)
                logging.info("BLIP SUPPOSED TO BE CREATED: %s" % db_blip)
                edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, 
                            wave_id = wave_id)                    
                blip = edited_wave.blips.get(db_blip.blip_id)
                if len(blip.text) < 2:
                    self._fill_blank_blip(blip)
                else:
                    annotation_list = blip.annotations.serialize()
                    ann = [a.pop('value') for a in annotation_list]    
                    if wave.wave_id in ann:
                        continue
                blip.at(2).insert("\n" + wave.title)
                blip.range(2, len(wave.title) + 3).annotate(View.WAVE_LINK_KEY, wave.wave_id)                
                self.__robot.submit(edited_wave)
        db_wave = Wave.objects.filter(wave_id = wave.wave_id)
        if not db_wave:
            db_wave = Wave(wave_id = wave.wave_id, title = wave.title, tags = wave.tags)
            db_wave.save()
        else:
            logging.debug("Wave exists.")
        self.__robot = None            


    def delete_blip(self, edited_wave, blip):
        """
            Removes empty blips starting from blip down to root_blip 
            (no waves and children blips inside them)
        """
        logging.info("DELETE blip: %s, blip.annotation.names(): %s, blip.child_blip_ids: %s" % 
                     (blip, blip.annotations.names(), blip.child_blip_ids))
        blip_id = blip.blip_id
        delete_catalog = []
        while not blip.is_root() and View.WAVE_LINK_KEY not in blip.annotations.names() and \
                                 not blip.child_blip_ids:
            delete_catalog.extend(list(Catalog.objects.filter(wave_id = edited_wave.wave_id, 
                                                              blip_id = blip.blip_id))) #i assume that db is syncronized with wave
            position = blip.inline_blip_offset
            parent_blip = blip.parent_blip
            edited_wave.delete(blip)
            blip = blip.parent_blip
            logging.info("blip_ID: %s, position: %d, parent_blip: %s" % (blip_id, position, parent_blip.blip_id))
        #remove label of tag from parent blip, reduce level annotation
        if blip.blip_id != blip_id:
            logging.info("POSITION:%d" % position)
           # last_removed_tag = Catalog.objects.get(wave_id = edited_wave.wave_id, blip_id = blip_id)
            #text = last_removed_tag.name
            logging.info("annotation: %s"  % blip.annotations.serialize())
            for ann in blip.annotations:
                if ann.name == View.LEVEL_KEY and position > ann.start and position <= ann.end: #ALSO REMOVE LVL label if necessary
                    logging.info("LEVEL. %s: %s, begin: %d, end: %d" % (ann.name, ann.value, ann.start, ann.end))
                    break  #ann is right annotation   
            is_empty = True        
            for i in range(ann.start, ann.end):
                if i != position and type(blip[i].value()) != unicode and \
                   blip[i].value().type == element.Element.INLINE_BLIP_TYPE:
                    logging.info("i: %d, blip_id: %s, blip[i].value().type: %s" % 
                                 (i, blip[i-10:i+10].value(), blip[i].value().type))
                    begin, end = self._get_tag_range(ann, blip, position)
                    logging.info("There are other blips")
                    is_empty = False
                    break
            if is_empty:
                logging.info("THERE are no inline blips inside this lvl, deleting the whole lvl")
                begin, end = ann.start, ann.end
            logging.info("RANGE: %d, %d, %s" % (begin, end, blip[begin:end].value()))
            logging.info("%s" % blip[27].value())
            blip[begin:end-1].delete()
        return delete_catalog


    def _get_tag_range(self, ann, blip = None, position = None):
        """
            position is a position inside line closer to right side (end of the line or its middle)
            position - 1 must be in the same line
        """
        if blip and position:
            begin1 = blip.text.rfind('\n', ann.start, position - 1)
            begin2 = begin1
            seq = range(max(begin1,ann.start), position)
            for i in seq[::-1]:
                if type(blip[i].value()) != unicode and blip[i].value().type == 'LINE':
                    begin2 = i
                    break
            begin = max(begin1, begin2)
            logging.info("blip[begin].value(): %s" % blip[begin].value())
            if abs(position - ann.end) < 2:
                end = ann.end
            else:
                if blip[position].value() == '\n' or type(blip[position].value()) != unicode and \
                                                     blip[position].value().type == 'LINE':
                    end = position
                else:
                    end1 = blip.text.find('\n', position, ann.end)
                    if end1 < 0:
                        end1 = ann.end
                    end2 = end1
                    for i in range(position, end1):
                        if type(blip[i].value()) != unicode and blip[i].value().type == 'LINE':
                            end2 = i
                            break
                    end = min(end1, end2)
        else:
            begin, end = ann.start, ann.end
        return (begin, end)


    def delete_wave(self, wave, level, category_list, ext_tag): 
        self._create_robot()
        path_list = wave.get_path(level, ext_tag)    
        wave_categories = wave.get_catalog_list(category_list)
        db_wave = Wave.objects.filter(wave_id = wave.wave_id)
        none_path = [None for i in path_list[-1]]
        delete_catalog = []
        for cat_index in wave_categories:
            wave_id = category_list[cat_index]['wave_id'].strip()
            for path in path_list: 
                logging.info("path: %s" %  path)
                blip_set = Catalog.objects.filter(path = path, wave_id = wave_id)
                db_blip = self._check_catalog_equality(blip_set, path)
                logging.info("db_blip: %s"  % unicode(db_blip))
                edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, wave_id = wave_id)                    
                blip = edited_wave.blips.get(db_blip.blip_id)
                annotation_list = blip.annotations.serialize()
                ann_range = [a.pop('range') for a in annotation_list]
                ann_value = [a.pop('value') for a in annotation_list]
                if wave.wave_id in ann_value:
                    index = ann_value.index(wave.wave_id)               
                    blip.range(ann_range[index]['start'], ann_range[index]['end']).delete()
                    #if path != none_path:      
                    delete_catalog = self.delete_blip(edited_wave, blip) #IMPLEMENT THIS              
                self.__robot.submit(edited_wave)
        self.__robot = None
        for cat in delete_catalog:
            cat.delete()
        for w in db_wave:            
            w.delete()
            
#---------------------NOT WORKING--------------------------------------------            
    def update_level(self, level):
        #needs to find 1. waves to change, 2. blips inside this waves containing reference to this lvl
        self._create_robot()
        #IMPLEMENT find wave_list
        for wave_id in wave_list:
            edited_wave = self._try_again(View.TRIES, self.__robot.fetch_wavelet, wave_id = wave_id)
            #IMPLEMENT find blip_list  
            for blip in blip_list:
                blip = edited_wave.blips.get(blip)
                for ann in blip.annotations:
                    if ann.name == View.LEVEL_KEY:
                        number = ann.value.split('.')
                        if level.level == int(number[0]):
                            ann_value = "%d.%s" % (level.level, level.name)
                            #finding name:
                            start = ann.start
                            for char in blip.text[ann.start:ann.end].value():
                                if type(char) == unicode and char != '\n':
                                    break
                                start += 1                            
                            level_name_range = self._get_tag_range(ann, blip, start + 1)
                            blip.range(ann.start, ann.end).annotate(View.LEVEL_KEY, 
                                                                    ann_value)
                            blip.range(level_name_range[0] + 1, level_name_range[1]).replace(level.name, 
                                                                                      [(View.BOLD_TEXT)])
            self.__robot.submit(edited_wave)
        self.__robot = None                        

                
    def add_waves(self, begin, level, category_list, ext_tag):
        logging.info("add_waves called!")
        i = 0
        for w in Wave.objects.filter(id__gt = begin).order_by('id'):
            wave = WaveEntry(w.wave_id, w.title, w.tags)
            logging.info("%s" % unicode(wave))
            self.add_wave(wave, level, category_list, ext_tag) 
            memcache.set('begin', w.id)
            if i < 5:
                i+=1
            else:
                break
        if i == 0:
            memcache.set('begin', 0)
            